iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0
Software Development

從 Vibe Coding 到可維護專案:用 Hermes Agent 重讀 RoomRush Android 專題系列 第 13 篇

Day 13 | 為什麼 ViewModel 不該直接碰資料庫?Repository 的角色是什麼?

  • 分享至 

  • xImage
  •  

前言:Repository 在資料層扮演什麼角色,為什麼 ViewModel 不直接碰 DAO?

前一篇介紹了 ClassroomScheduleDao.kt 的 CRUD 操作,很多人可能會好奇:「既然 DAO 已經能抓資料,為什麼 ViewModel 不直接找 DAO,還要多夾一層 Repository?」

簡單來說,兩者的分工就像是:

  • ViewModel :只管「畫面要呈現什麼」,不在乎資料到從哪裡來。
  • Repository :專門負責「找資料」。它幫 ViewModel 擋在前面,統一調度底層的資料來源。

今天的主角 ClassroomScheduleRepository.kt,正是站在 DAO 與 ViewModel 之間,負責管理所有課表資料流向的關鍵橋樑。


一開始我認為

一開始我也不太理解為什麼需要另外寫一個 Repository 來作為資料調度員,而不是直接寫在 ViewModel 裡。不過實際去了解其用意後,我對 現代 Android MVVM 更有概念。

在 Android 官方架構中,讓 Repository 當作中間的資料調度員,主要有三個關鍵原因:

  • 職責分離(隱藏來源) :ViewModel 只管「邏輯與狀態」,不該知道資料是來自 Room 資料庫、遠端 API 還是 記憶體快取。
  • 統一調度多重來源 :未來若要支援「先抓網路 API,沒網才讀本地 Room」,只要在 Repository 整合網路與本地資料庫,ViewModel 完全不用改程式碼。
  • 方便寫單元測試(解耦) :測試 ViewModel 時,直接假造(Mock)Repository 就能快速測試 ViewModel 的邏輯,不需要真的建一個資料庫。

另外,原本我以為,只要看到 ViewModel → Repository → DAO,就代表這個專案已經把資料存取分層做好了。

但實際重新追程式碼後,我發現事情沒有這麼單純:QueryResultViewModel 確實會透過 Repository 取得資料,但部分查詢與篩選邏輯仍然直接寫在 ViewModel 裡,而且有些邏輯和其他地方存在重複。


實際讀完後,ClassroomScheduleRepository.kt負責什麼

和 Hermes Agent 一起重讀檔案後,我了解 ClassroomScheduleRepository.kt 是負責把 ClassroomScheduleDao的資料操作包裝起來,提供方法給 ViewModel 使用,讓 ViewModel 不需要直接接觸 DAO 或資料庫操作細節。

1. 作為全課表資料的流向入口

class ClassroomScheduleRepository(private val dao: ClassroomScheduleDao) {
fun getAllSchedules(): Flow<List<ClassroomSchedule>> = dao.getAll()
}
  • 技術細節 :Repository 本身不寫 SQL,而是透過 DAO 完成資料操作。getAllSchedules()是取得所有教室課表;可能被 QueryResultViewModel 使用,因為是查詢頁的邏輯。

2. 與 ViewModel 重疊的篩選邏輯(技術債)

suspend fun queryEmptyRooms(
buildingName: String, day: String, timeSlotKey: String, floorQueryValue: String
): List<ClassroomSchedule> {
val schedules = dao.getSchedulesByBuildingAndFloor("$buildingName$floorQueryValue%")
return schedules.filter { schedule ->
getScheduleStatus(schedule, "${day.lowercase()}$timeSlotKey") == "O"
}
}
  • 技術細節與反思 :這段會先用 DAO 查某棟某層的教室,再根據指定時段欄位留下狀態為 O 的教室。 但實際上,QueryResultViewModel 內部自己也做了一次空教室過濾。
    這就是典型的職責邊界混淆(邏輯重複實作),也是後續架構重構時最值得拔除的技術債之一。

3. 提供詳細頁所需的單一教室資料流

fun getClassroomDetails(classRoomName: String): Flow<ClassroomSchedule?> {
return dao.getClassroomDetails(classRoomName)
}
  • 技術細節 :供 RoomDetailViewModel 呼叫,用來根據教室名稱取得該教室資料。

Entity、DAO、Repository:每一層到底負責什麼?

理想上的資料存取路徑:

ViewModel
「提出資料需求」
    │
    ▼
Repository
「協調資料來源」
    │
    ▼
DAO
「執行資料庫操作」
    │
    ▼
Room Database
「實際儲存資料」

這樣的分層可以讓不同元件各自負責不同工作:ViewModel 處理畫面所需的狀態,Repository 負責協調資料來源,DAO 負責資料庫操作,而 Room Database 負責實際儲存資料。

但 RoomRush 目前:

QueryResultViewModel
「接收查詢條件、處理部分篩選邏輯」
        ↓
Repository
「協調資料存取」
        ↓
DAO
「執行資料庫操作」
        ↓
Room Database
「實際儲存資料」

在這裡,QueryResultViewModel 除了接收查詢條件,也負責了一部分空教室的篩選邏輯。

從架構上來看,ViewModel → Repository → DAO 是一條清楚的資料存取路徑。不過重新讀過 RoomRush 後,我發現實際的職責沒有完全切開。


Hermes Agent 幫我檢查出的重點

ClassroomScheduleRepository 是 ViewModel 和 DAO 中間的資料層中介。

它持有 ClassroomScheduleDao,並提供 getAllSchedules()、insert()、insertAll()、update()、delete()、findByName()、getClassroomDetails() 等方法給 ViewModel 使用。多數方法只是薄薄包裝 DAO,但這樣可以讓 ViewModel 不直接接觸 DAO。

值得注意的是,Repository 裡也有 queryEmptyRooms(),但目前查詢頁主要是在 QueryResultViewModel 裡自己篩選 emptyRooms,代表查詢邏輯有分散與可能重複的情況。


這個檔案帶出的維護觀察

有些查詢邏輯在 Repository 裡,有些在 QueryResultViewModel 裡,可能有重複和分散的部分。
特別是在, Repository 的 queryEmptyRooms() 用 == "O",而 QueryResultViewModel 用 != "X",兩者邏輯接近但不完全一致,未來可能需要統一。

  1. 職責分散,缺乏單一真實來源:空教室的過濾邏輯沒有完全收斂在資料層,部分寫在 Repository,部分又跑到了 QueryResultViewModel,造成規則散落各處。
  2. 判定標準有衝突:
    • Repository(白名單): 嚴格判定欄位值必須 == "O" 才算可用。
    • QueryResultViewModel(排除法): 只要欄位值 != "X" 就一律視為可用。

乍看之下兩者差不多,但可能導致邊界情境(Edge Case):如果某個時段的資料解析出現 null、空白字串或異常代碼:

  • 用 Repository 的邏輯查:不是 "O",判定 不能用。
  • 用 QueryResultViewModel 的邏輯查:不是 "X",判定 可以用 。

理想的重構作法是定義明確的狀態標籤(例如 AVAILABLE 代表可用、BUSY 代表佔用),不要再用字串 "O" 和 "X";並把所有過濾規則收斂回 Repository 內,確保整個專案永遠只遵照同一個「單一真實來源」。


小結

讀完 ClassroomScheduleRepository 後,我更清楚 RoomRush 的資料層分工。

DAO 定義了實際的資料庫操作,而 Repository 則把 DAO 包裝成 ViewModel 可以使用的方法。這樣 ViewModel 不需要直接知道 SQL 或 DAO 細節,只要透過 Repository 取得資料。

不過這個檔案也讓我看到一個值得記錄的技術債:Repository 裡有 queryEmptyRooms(),但目前查詢頁的主要空教室篩選邏輯是在 QueryResultViewModel 裡完成。

也就是說,查詢邏輯有一部分分散在不同層。未來如果要讓專案更容易維護,可以思考是否要把查詢規則集中到 Repository,或至少統一「可用教室」的判斷條件。

簡單用一句話總結這個檔案:

ClassroomScheduleRepository 包裝 DAO,讓 ViewModel 透過 Repository 取得與操作教室課表資料。


下一篇預告:CSV 檔案是怎麼變成 App 裡可以查的課表資料?

前面我們已經了解 ClassroomScheduleRepository.kt 如何透過 DAO 操作資料。

但下一個問題隨之而來: AppDatabase.kt 到底是怎麼把 SF.csv/ES.csv 裡的原始課表資料,變成 Room Database 裡的 ClassroomSchedule?

下一篇我們會繼續往後追資料來源,把這段看懂之後,就能真正串起「原始 CSV 課表 → Room Database → App 查詢」的資料流。


上一篇
Day 12 | App 要查資料、改資料,Room DAO 負責哪一層?
下一篇
Day 14 | CSV 檔案是怎麼變成 App 裡可以查的課表資料?
系列文
從 Vibe Coding 到可維護專案:用 Hermes Agent 重讀 RoomRush Android 專題 共 23 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言